1、 tty 驱动程序框架
	tty 驱动程序从下往上分别是设备驱动层、行规程、终端虚拟化、TTY I/O层，它们的功能如下：
	设备驱动层：用于驱动设备，如串口、显示器、键盘等
	行规程：用于处理控制字符、回显输入数据、缓存输入数据、显示数据输出，如果应用层不需要这些处理机制可以将行
规程设置为原始模式
	终端虚拟化：用于虚拟化显示器、键盘、鼠标组成的终端
	TTY I/O层：与应用层进行交互
2、 uart_driver 对象
	uart_driver 对象表示一个 uart 驱动，其核心成员如下：
	//所属模块
	struct module *owner;
	//驱动名称
	const char *driver_name;
	//设备名称，串口设备文件名以此为基础生成
	const char *dev_name;
	//主设备号，为0表示系统自动分配
	int major;
	//起始次设备号，即第一个串口的次设备号
	int minor;
	//此驱动支持的串口个数
	int nr;
	//若驱动支持 console 则指向对应的 serial console ，否则为 NULL
	struct console *cons;
	//串口的状态信息，每个串口设备都有自己的状态信息，其中主要包括 tty_port 和 uart_port
	struct uart_state *state;
	//串口驱动对应的 tty 驱动
	struct tty_driver *tty_driver;
3、 console 对象
	console 对象用于描述一个控制台驱动，其核心成员如下：
	//名字
	char name[16];
	//写函数
	void (*write)(struct console *, const char *, unsigned);
	//读函数
	int	(*read)(struct console *, char *, unsigned);
	//获取对应的 tty 驱动
	struct tty_driver *(*device)(struct console *, int *);
	//设置 console
	int (*setup)(struct console *, char *);
	//console 标志
	short flags;
	//console 索引，若小于 0 则由命令行参数确定
	short index;
4、 uart_state 对象
	uart_state 对象表示一个串口设备的状态信息，其核心成员如下：
	//串口所属的 tty 端口，主要包含 tty 端口的 buf 、端口操作函数等信息
	struct tty_port port;
	//串口端口，对应一个串口设备，主要包含串口的硬件操作函数等信息
	struct uart_port *uart_port;
5、 uart_port 对象
	uart_port 对象表示一个串口端口，其核心成员如下：
	//配置 RS485
	int (*rs485_config)(struct uart_port *, struct serial_rs485 *rs485);
	//中断号
	unsigned int irq;
	//串口基准时钟
	unsigned int uartclk;
	//发送 FIFO 大小
	unsigned int fifosize;
	//流控字符
	unsigned char x_char;
	//IO 类型
	unsigned char iotype;
	//串口标志
	upf_t flags;
	//操作函数集合
	const struct uart_ops *ops;
	//端口索引
	unsigned int line;
	//寄存器逻辑基地址
	resource_size_t mapbase;
	//寄存器映射大小
	resource_size_t mapsize;
	//所属父设备
	struct device *dev;
	//RS485 配置信息
	struct serial_rs485 rs485;
6、 tty_driver 对象
	tty_driver 对象用于表示一个 tty 驱动，其核心成员如下：
	//幻数，用于检查结构体是否是一个 tty_driver
	int magic;
	//cdev 指针数组，用于关联 tty_port 的字符设备驱动
	struct cdev **cdevs;
	//所属模块
	struct module *owner;
	//驱动名称
	const char *driver_name;
	//设备名称，对于非 TTY_DRIVER_TYPE_PTY 类型的 tty_port ，其设备文件名以此为基础生成
	const char *name;
	//主设备号，为0表示系统自动分配
	int major;
	//起始次设备号，即第一个 tty_port 的次设备号
	int minor_start;
	//此驱动支持的 tty_port 个数
	unsigned int num;
	//tty 类型
	short type;
	//tty 子类型
	short subtype;
	//初始配置参数
	struct ktermios init_termios;
	//tty 驱动标志
	unsigned long flags;
	//tty_struct 指针数组
	struct tty_struct **ttys;
	//tty_port 指针数组
	struct tty_port **ports;
	//ktermios 指针数组
	struct ktermios **termios;
	//tty 驱动操作函数接口
	const struct tty_operations *ops;
7、串口驱动注册过程
	1）注册串口驱动
		构建 uart_driver 对象，并进行相应的设置
		调用 uart_register_driver 注册 uart_driver
			为 uart_driver 分配 uart_state
			调用 alloc_tty_driver 分配一个 tty_driver
				tty_alloc_driver
					__tty_alloc_driver
						分配 tty_driver
						进行一些基本的初始化
						为 tty_driver 分配 tty_struct 、 ktermios 、 tty_port 、 cdev 指针数组
			关联 uart_driver 与 tty_driver
			利用 uart_driver 的参数配置 tty_driver
			调用 tty_set_operations 设置 tty_driver 的操作函数集合
			依次调用 tty_port_init 初始化 uart_state 中的 tty_port ，然后绑定 tty_port 操作函数集合
			调用 tty_register_driver 注册 tty_driver
	2）添加端口
		注册 uart_driver 对象，构建 uart_port 对象，并进行相应的设置
			调用 uart_add_one_port 在 uart_driver 中添加一个 uart_port
				将 uart_driver 的 uart_state 与 uart_port 进行相互关联
				配置 uart_port 的次设备号、名字、 cons 等
				调用 tty_port_link_device 将 uart_state 的 tty_port 与 tty_driver 进行关联
				调用 uart_configure_port 对 uart_port 进行一些配置
					如果 uart_port 的 iobase 、 mapbase 、 membase 均为 0 则直接退出
					如果 uart_port 的 flags 设置了 UPF_BOOT_AUTOCONF 则调用 config_port 函数配置串口硬件
					如果端口有 console 且还未使能则调用 register_console 注册 console
				设置 tty_port 的 console 标志（需要 uart_por t的 line 与 console 的 index 一致才设置）
				调用 tty_port_register_device_attr_serdev 注册 tty_port
					调用 tty_register_device_attr 注册 tty 设备
						调用 tty_line_name 生成设备文件名
						分配一个 device 对象，并对其初始化，这里配置了设备号和设备名，注册后会创建设备文件
						调用 device_register 注册分配的 device
						调用 tty_cdev_add 注册字符设备驱动
							分配 cdev 对象，并进行初始化，主要配置操作函数集合
							调用 cdev_add 将分配 cdev 对象注册到内核
8、串口打开过程
	添加端口的时候会注册一个 cdev ，并设置其操作函数集合为 tty_fops ，其中提供了 tty_open 函数，在应用层执行
open 时会通过虚拟文件系统调用到此函数，其执行过程如下：
	在应用层执行 open 时会通过虚拟文件系统调用 tty_open
		执行 tty_open_current_tty 尝试打开进程所属的 tty （其设备文件名应该是/dev/tty，设备ID应该是MKDEV(5, 
	0)），对于打开串口此函数会执行失败
		tty_open_current_tty 执行失败后会执行 tty_open_by_driver 来打开 tty
			执行 tty_lookup_driver 找到对应的 tty_driver
			执行 tty_driver_lookup_tty 从 tty_driver 找到对应的 tty_struct ，对于打开过的 tty 会执行成功
			若 tty_driver_lookup_tty 执行失败则调用 tty_init_dev 分配 tty_struct 并进行相应的配置
				调用 alloc_tty_struct 分配 tty_strct
					调用 kzalloc 分配 tty_strct
					调用 tty_ldisc_init 绑定行规程
				调用 tty_driver_install_tty 将 tty_strct 安装到 tty_driver 中
				调用 tty_ldisc_setup 对配置行规程
			通过 tty->ops->open 指针调用 serial_core 的 uart_open
				调用 tty_port_open 打开对应端口
					通过 port->ops->activate 调用 serial_core 的 uart_port_activate 函数
						调用 uart_startup 启动串口
							调用 uart_port_startup 启动串口
								通过 uport->ops->startup 调用串口驱动提供的 startup 函数
9、串口读过程
	读串口数据可分为两部分：
	1）应用程序从行规程中读取数据
		与 open 过程类似，在应用层执行 read 时会通过虚拟文件系统调用 tty_read 函数，其执行过程如下：
		在应用层执行 read 时会通过虚拟文件系统调用 tty_read
			通过 tty_ldisc_ref_wait 得到 tty 的行规程
			通过 ld->ops->read 调用行规程的 read 函数，这里应该是 n_tty 中的 n_tty_read 函数
				在 n_tty_read 中无数据则休眠，有数据则将数据拷贝到应用层
	2）串口向行规程上报数据
		串口收到数据进入中断程序
			中断程序从硬件读取数据
			调用 tty_insert_flip_string 将数据存入 tty_port 的 tty_buffer 中
			调用 tty_flip_buffer_push 通知行规程处理数据
				调用 tty_schedule_flip 启动数据处理
					调用 queue_work 启动一个工作队列处理数据，工作队列对应的处理函数为 flush_to_ldisc
10、串口写数据
	在应用层执行 write 时会通过虚拟文件系统调用 tty_write 函数，然后 tty_write 函数通过绑定的行规程调用到行
规程的 n_tty_write 函数，接下来由行规程对数据处理后调用 serial_core 的 uart_flush_chars 启动发送，如果行规程
设置原始模式则不进行处理，直接调用 serial_core 的 uart_write 函数启动发送（发送过程由串口的中断或 DMA 完成），
其流程如下：
	在应用层执行 write 时会通过虚拟文件系统调用 tty_write 函数
		通过 tty_ldisc_ref_wait 获得与tty绑定的行规程
		调用 do_tty_write 执行数据发送操作（传入行规程的 write 函数作为实际的写函数）
			将数据拷贝到 write_buf 中
			通过指针调用行规程的 write 函数，这里对应的是 n_tty_write 函数
				如果设置了 OPOST 标志则对数据进行处理后调用 tty 操作函数集合中的 flush_chars 函数发送数据，这
			里实际对应的是 uart_flush_chars 函数
					最后调用 uart_start 启动串口发送
						调用 __uart_start 函数
							调用 uart_port 中的 start_tx 函数，即硬件驱动提供的发送启动函数
				如果没有设置 OPOST 标志则调用 tty 操作函数集合中的 write 函数发送数据，这里 实际对应的是 
			uart_write 函数
					将数据写入到 uart_state 的 buf 中后在调用 __uart_start 启动串口发送
						调用 uart_port 中的 start_tx 函数，即硬件驱动提供的发送启动函数
		通过 tty_ldisc_deref 释放对行规程的占用
11、 console 注册过程
	1）构建并初始化 console 对象
	2）将 console 对象的地址给 uart_driver 的 cons 成员
	3）调用 uart_add_one_port 函数时完成对 console 的注册，注册流程如下：
		将 uart_driver 的 cons 赋给 uart_port 的 cons
		调用 uart_configure_port
			若 uart_port 的 cons 有效，且 cons 未使能则调用 register_console 注册 console
				利用启动命令行中的 console_cmdline 参数进行匹配，匹配成功则设置 console 的 index ，然后调用
			setup 函数进行配置
				将 console 添加到 console_drivers 链表中
		当 uart_port 的 line 与 console 的 index 一致则设置 tty_port 的 console 标志,因此在多个 uart_port 中
	只有一个 uart_port 真正的拥有 uart_driver 的 console
12、串口驱动框架
	串口驱动包含以下几个要素：
		1）构建并初始化 uart_driver
		2）注册 uart_driver
		3）构造并初始化 uart_port
		4）向 uart_driver 添加 uart_port
13、 printk 执行流程
	printk 的执行流程大致如下：
		通过 va_start 取出不定参数列表，然后调用 vprintk_func
			然后调用 vprintk_default 函数
				再调用 vprintk_emit 函数
					调用 vprintk_store 函数
						使用 vscnprintf 进行格式化处理
							调用 log_output 输出数据（并未通过硬件输出，实际上是将数据存储在 log_buf 中）
					调用 console_unlock 函数，可能是直接调用，也可能是唤醒工作队列去调用
						通过 log_from_idx 从 log_buf 取出一个 msg
						如果 msg 优先级不够则跳过
						通过 call_console_drivers 将数据从 console 输出，系统中可能会有多个 console
14、 printk 使用
	格式：
	/*
	 * fmt 格式字符串，其前面还包含描述优先级的字符，它们的定义如下：
	 *     ASCII的标题开始字符（SOH），表示后面是优先级字符
	 *     #define KERN_SOH			"\001"
	 *     不同优先级定义，数字越小优先级越高
	 *     #define KERN_EMERG		KERN_SOH "0"
	 *     #define KERN_ALERT		KERN_SOH "1"
	 *     #define KERN_CRIT		KERN_SOH "2"
	 *     #define KERN_ERR			KERN_SOH "3"
	 *     #define KERN_WARNING		KERN_SOH "4"
	 *     #define KERN_NOTICE		KERN_SOH "5"
	 *     #define KERN_INFO		KERN_SOH "6"
	 *     #define KERN_DEBUG		KERN_SOH "7"
	 * ... 不定参数列表，与格式字符串有关
	 **/
	int printk(const char *fmt, ...)
	设置系统的输出优先级
		系统输出等级存储在 /proc/sys/kernel/printk 文件中，它有4个参数，依次是：控制台消息级别、默认信息级别
	、最小控制台级别、默认控制台级别
		控制台消息级别：当输出的消息级别值小于此值时才从控制台输出
		默认信息级别：未指定消息级别时采用的默认级别
15、 early_printk
	当在命令行参数中传入 earlyprintk 且在内核配置选项中使能 Early printk 选项后系统会调用 setup_early_printk 
函数创建一个 console （此 console 的 write 函数最终会调用 printascii 输出数据， STM 官方内核已经通过汇编实现
printascii 函数），用于在串口初始化完成以前输出调试信息，具体使用步骤如下：
	1）配置内核
		Kernel hacking  ---> 
			[*] Kernel low-level debugging functions (read help!)
			(0x40010000) Physical base address of debug UART (NEW)
			(0xfe010000) Virtual base address of debug UART (NEW)
			[*] Early printk
	2）在 bootargs 中加入 earlyprintk
	earlyprintk 注册过程：
	当 bootargs 中包含 earlyprintk 时在系统初始化阶段会调用 setup_early_printk
		调用 register_console 注册一个 console ，用于系统早期输出（通过 console 通过的 write 函数输出）
16、 earlycon
	earlycon 与 early_printk 类似，都是用于系统启动初期输出一些调试信息，当在 bootargs 参数中传入 earlycon 
后 param_setup_earlycon 函数会被调用，它根据 earlycon 后面携带的参数或设备树创建一个 console ，其使用方法如下：
	方法1：在 bootargs 中增加 earlycon 并附带匹配名称、设备地址、配置参数等信息
	方法2：在 bootargs 中增加 earlycon，在设备树中增加 stdout-path 属性
	注意：需要使用 OF_EARLYCON_DECLARE(_name, compat, fn) 宏向 __earlycon_table 列表添加对应的 earlycon_id
	earlycon 注册过程：
	bootargs 参数中传入 earlycon 后 param_setup_earlycon 函数会在系统初始化阶段被调用
		如果 earlycon 未携带参数则执行 early_init_dt_scan_chosen_stdout 函数
			找到设备树 chosen 节点中的 stdout-path 或 linux,stdout-path 属性
			遍历 __earlycon_table 列表，找到与 stdout-path 或 linux,stdout-path 所引用的节点匹配的 earlycon_id
			遍历成功调用 of_setup_earlycon ，从设备树中获取一些信息
				对 early_console_dev 进行基本初始化
				调用 earlycon_id 的 setup 函数设置 early_console_dev
				调用 register_console 注册一个 console
		如果 earlycon 携带参数则执行 setup_earlycon
			遍历 __earlycon_table 列表，找到与参数匹配的 earlycon_id（bootargs 的 earlycon 应携带设备名称）
			遍历成功调用 register_earlycon
				对 early_console_dev 进行基本初始化
				调用 earlycon_id 的 setup 函数设置 early_console_dev
				调用 register_console 注册一个 console
